/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.service.impl.data.validation;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.MeasurementCategoryElement;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.service.impl.ModelElementValidator;
import org.unidata.mdm.meta.type.model.PeriodBoundary;
import org.unidata.mdm.meta.type.model.SimpleDataType;
import org.unidata.mdm.meta.type.model.attributes.AttributeMeasurementSettings;
import org.unidata.mdm.meta.type.model.attributes.SimpleMetaModelAttribute;
import org.unidata.mdm.meta.type.model.entities.AbstractEntity;
import org.unidata.mdm.meta.util.ValidityPeriodUtils;
import org.unidata.mdm.system.exception.ValidationResult;
import org.unidata.mdm.system.util.ConvertUtils;

public abstract class AbstractDataModelElementValidator<V extends Serializable> implements ModelElementValidator<V> {

    protected static final String DOT = ".";

    protected static final Set<String> RESERVED_NAMES = new HashSet<>(Arrays.asList("model", "audit", "classifier"));

    private static final String MODEL_ELEMENT_WITHOUT_ID = "app.meta.element.without.id";

    private static final String MODEL_ELEMENT_NAME_INVALID_CHARACTERS = "app.meta.element.name.invalid.characters";

    private static final String SIMPLE_ATTRIBUTE_WITHOUT_TYPE = "app.meta.simple.attribute.without.type";

    private static final String SIMPLE_ATTRIBUTE_LINK_TYPE = "app.meta.simple.attribute.link.type";

    private static final String SIMPLE_ATTRIBUTE_NAME_DOT = "app.meta.simple.attribute.name.dot";

    private static final String SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_MISSING = "app.meta.simple.attribute.measurement.settings.missing";

    private static final String SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_NO_CATEGORY = "app.meta.simple.attribute.measurement.settings.no.category";

    private static final String SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_NO_UNIT = "app.meta.simple.attribute.measurement.settings.no.unit";

    private static final String SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_NOT_ALLOWED = "app.meta.simple.attribute.measurement.settings.not.allowed";

    private static final String SIMPLE_ATTRIBUTE_FLAGS_REQUIRED_AND_READ_ONLY = "app.meta.simple.attribute.required.and.read.only";

    private static final String SIMPLE_ATTRIBUTE_FLAGS_HIDDEN_AND_NOT_READ_ONLY = "app.meta.simple.attribute.hidden.not.read.only";

    private static final String SIMPLE_ATTRIBUTE_FLAGS_MAIN_DISPLAYABLE_NOT_DISPLAYABLE = "app.meta.simple.attribute.main.displayable.not.displayable";

    private static final String SIMPLE_ATTRIBUTE_FLAGS_DISPLAYABLE_HIDDEN = "app.meta.simple.attribute.displayable.hidden";

    private static final String ENTITY_GROUP_IS_ABSENT = "app.meta.entity.group.absent";

    private static final String ENTITY_RESERVED_TOP_LEVEL_NAME = "app.meta.entity.reserved.toplevel.name";

    private static final String ENTITY_PERIOD_START_BEFORE_GLOBAL_PERIOD = "app.meta.entity.period.start.before.global";

    private static final String ENTITY_PERIOD_END_AFTER_GLOBAL_PERIOD = "app.meta.entity.period.end.after.global";

    private static final String ENTITY_MAIN_DISPLAYABLE_ATTRIBUTE_ABSENT = "app.meta.entity.main.displayable.attribute.absent";

    @Autowired
    protected MetaModelService metaModelService;

    protected abstract String getModelElementId(V modelElement);

    protected Collection<ValidationResult> checkSimpleAttribute(SimpleMetaModelAttribute sa, String entityName) {

        Collection<ValidationResult> errors = new ArrayList<>();

        boolean hasDataType = sa.getSimpleDataType() != null;
        boolean isLinkType = sa.getSimpleDataType() == SimpleDataType.STRING || sa.getSimpleDataType() == null;
        boolean isLink = StringUtils.isNotBlank(sa.getEnumDataType()) || StringUtils.isNotBlank(sa.getLookupEntityType()) || StringUtils.isNotBlank(sa.getLinkDataType());
        boolean isDictionaryType = sa.getDictionaryDataType() != null || !sa.getDictionaryDataType().isEmpty();

        if (!hasDataType && !isLink && !isDictionaryType) {
            final String message = "Simple attribute [{}] in [{}] is not a link, enum, dictionary or lookup reference and has no valid data type.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_WITHOUT_TYPE, sa.getName(), entityName));
        }

        if (isLink && !isLinkType) {
            final String message = "Simple attribute [{}] in [{}] is a link, enum, dictionary or lookup reference and has invalid data type set.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_LINK_TYPE, sa.getName(), entityName));
        }

        if (sa.getName().contains(DOT)) {
            final String message = "Simple attribute [{}] in [{}] contains '.' in its name, which is not allowed.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_NAME_DOT, sa.getName(), entityName));
        }

        if (sa.getSimpleDataType() == SimpleDataType.MEASURED) {
            errors.addAll(checkMeasuredAttribute(sa, entityName));
        } else if (sa.getMeasureSettings() != null) {
            final String message = "Simple attribute [{}] in [{}] is not of type MEASURED but does define measurement settings.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_NOT_ALLOWED, sa.getName(), entityName));
        }

        errors.addAll(checkFlagsCombinations(sa, entityName));

        return errors;
    }

    protected Collection<ValidationResult> checkMeasuredAttribute(SimpleMetaModelAttribute sa, String entityName) {

        Collection<ValidationResult> errors = new ArrayList<>();

        AttributeMeasurementSettings ms = sa.getMeasureSettings();
        String categoryId = ms == null ? null : ms.getCategoryId();
        String unitId = ms == null ? null : ms.getDefaultUnitId();
        if (ms == null || StringUtils.isBlank(categoryId) || StringUtils.isBlank(unitId)) {
            final String message = "Measured simple attribute [{}] in [{}] does not define valid measurement settings.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_MISSING, sa.getName(), entityName));
        } else {

            MeasurementCategoryElement value = metaModelService
                    .instance(Descriptors.MEASUREMENT_UNITS)
                    .getCategory(categoryId);

            if (value == null) {
                final String message = "Measured simple attribute [{}] in [{}] refers to an undefined MU category [{}].";
                errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_NO_CATEGORY,
                        sa.getName(), entityName, categoryId));
            } else if (!value.exists(unitId)) {
                final String message = "Measured simple attribute [{}] in [{}] refers to an undefined measurement unit [{}].";
                errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_MEASUREMENT_SETTINGS_NO_UNIT,
                        sa.getName(), entityName, unitId));
            }
        }

        return errors;
    }

    protected Collection<ValidationResult> checkFlagsCombinations(SimpleMetaModelAttribute sa, String entityName) {

        Collection<ValidationResult> errors = new ArrayList<>();

        //UN-4534
        if (!sa.isNullable() && sa.isReadOnly()) {
            final String message = "Simple attribute [{}] in [{}] has both 'nullable' and 'readOnly' flags set.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_FLAGS_REQUIRED_AND_READ_ONLY, sa.getName(), entityName));
        }

        if (sa.isHidden() && !sa.isReadOnly()) {
            final String message = "Simple attribute [{}] in [{}] has both 'hidden' flag set but is not 'readOnly'.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_FLAGS_HIDDEN_AND_NOT_READ_ONLY, sa.getName(), entityName));
        }

        if (sa.isMainDisplayable() && !sa.isDisplayable()) {
            final String message = "Simple attribute [{}] in [{}] has both 'mainDisplayable' flag set but is not 'displayable'.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_FLAGS_MAIN_DISPLAYABLE_NOT_DISPLAYABLE,
                    sa.getName(), entityName));
        }

        if (sa.isDisplayable() && sa.isHidden()) {
            final String message = "Simple attribute [{}] in [{}] has both 'displayable' flag set but is 'hidden'.";
            errors.add(new ValidationResult(message, SIMPLE_ATTRIBUTE_FLAGS_DISPLAYABLE_HIDDEN, sa.getName(), entityName));
        }

        return errors;
    }

    protected Collection<ValidationResult> checkEntitiesGroupValue(String value, String entityName) {

        if (StringUtils.isBlank(value)) {
            final String message = "Entities group value is not defined for top level element [{}].";
            return Collections.singleton(new ValidationResult(message, ENTITY_GROUP_IS_ABSENT, entityName));
        }

        return Collections.emptyList();
    }

    protected Collection<ValidationResult> checkTopLevelNameStopList(String value, String entityName) {

        if (RESERVED_NAMES.contains(value)) {
            final String message = "Top level element [{}] uses one of reserved names [{}].";
            return Collections.singleton(new ValidationResult(message, ENTITY_RESERVED_TOP_LEVEL_NAME, entityName, value));
        }

        return Collections.emptyList();
    }

    protected Collection<ValidationResult> checkMainDisplayableState(boolean value, String entityName) {

        if (!value) {
            final String message = "Top level element [{}] does not define a main displayable attribute.";
            return Collections.singleton(new ValidationResult(message, ENTITY_MAIN_DISPLAYABLE_ATTRIBUTE_ABSENT, entityName));
        }

        return Collections.emptyList();
    }

    protected Collection<ValidationResult> checkValidityPeriod(PeriodBoundary period, String entityName) {

        Collection<ValidationResult> errors = new ArrayList<>(2);

        Date start = ValidityPeriodUtils.getGlobalValidityPeriodStart();
        Date end = ValidityPeriodUtils.getGlobalValidityPeriodEnd();

        if (Objects.nonNull(start) && Objects.nonNull(period) && Objects.nonNull(period.getStart())
         && start.after(ConvertUtils.localDateTime2Date(period.getStart()))) {
            final String message = "Validity period of a top level element [{}] begins before global validity period start [{}].";
            errors.add(new ValidationResult(message, ENTITY_PERIOD_START_BEFORE_GLOBAL_PERIOD, entityName, ValidityPeriodUtils.asString(start)));
        }

        if (Objects.nonNull(end) && Objects.nonNull(period) && Objects.nonNull(period.getEnd())
         && end.before(ConvertUtils.localDateTime2Date(period.getEnd()))) {
            final String message = "Validity period of a top level element [{}] ends after global validity period end [{}].";
            errors.add(new ValidationResult(message, ENTITY_PERIOD_END_AFTER_GLOBAL_PERIOD, entityName, ValidityPeriodUtils.asString(end)));
        }

        return errors;
    }

    @Override
    public Collection<ValidationResult> checkElement(V modelElement) {

        Collection<ValidationResult> errors = new ArrayList<>(2);

        String id = getModelElementId(modelElement);
        if (StringUtils.isBlank(id)) {
            errors.add(new ValidationResult("Model element does not have an ID (name).", MODEL_ELEMENT_WITHOUT_ID));
        }

        if (id.contains(DOT)) {
            errors.add(new ValidationResult("Model element name [{}] contains invalid character [.].",
                    MODEL_ELEMENT_NAME_INVALID_CHARACTERS,
                    modelElement instanceof AbstractEntity ? ((AbstractEntity<?>) modelElement).getName() : "UNKNOWN"));
        }

        return errors;
    }
}
